
/*! Ruotong Filters v3 — quieter, tidier, smarter
 *  - Internal tabs (风格/情绪/人声/乐器/BPM/时长/调式)
 *  - Quiet counts (show on hover/selected), compact density
 *  - Selected-first + smart ordering (by counts), "最近使用"
 *  - Collapsible group search (点击“搜索”显示)
 *  - Undo last change（撤销上一步），空结果时强调
 *  - Dynamic facet counts + disable zero-result
 *  - Non-destructive: only touches #pl-chips and parses .table
 */
(function(){
  const mount = document.getElementById('pl-chips');
  const table = document.querySelector('.table');
  if(!mount || !table) return;

  /* ========== Dictionaries ========== */
  const EN2CN = new Map([
    ['Acoustic','原声'], ['Folk','民谣'], ['Classical','古典'], ['Contemporary Classical','当代古典'],
    ['Indie Folk','独立民谣'], ['Pop','流行'], ['Indie Pop','独立流行'], ['Beats','节拍/伴奏'],
    ['Hip Hop','嘻哈'], ['Lo-fi Hip Hop','Lo‑fi 嘻哈'],
    ['Ambient','氛围'], ['Cinematic','影视配乐'], ['Jazz','爵士'],
    ['Cool Jazz','冷爵士/酷派爵士'], ['Classic Jazz','传统爵士'], ['House','浩室'],
    ['Singer-songwriter','创作歌手'], ['Solo Piano','钢琴独奏'], ['Solo Guitar','吉他独奏'],
    ['Drama','戏剧性'], ['Build','推进/铺垫'], ['Pulses','律动'], ['Beautiful','唯美'],
    ['Boom Bap','砰砰啪（老派嘻哈）'], ['Classical Crossover','古典跨界'],
    // defensive additions
    ['Vocal','人声'], ['Female','女声'], ['Male','男声'], ['Choir','合唱'], ['Voice','人声'],
    ['Piano','钢琴'], ['Guitar','吉他'], ['Strings','弦乐'], ['Synth','合成器'], ['Percussion','打击'],
    ['Woodwinds','木管'], ['Brass','铜管'], ['Bass','贝斯']
  ]);

  const TAGS = {
    '风格': [
      '原声','民谣','古典','当代古典','独立民谣','流行','独立流行','节拍/伴奏','嘻哈','Lo‑fi 嘻哈','氛围','影视配乐',
      '爵士','冷爵士/酷派爵士','传统爵士','浩室','创作歌手','钢琴独奏','吉他独奏','戏剧性','推进/铺垫','律动','唯美',
      '砰砰啪（老派嘻哈）','古典跨界'
    ],
    '情绪': ['舒缓','温暖','明亮','活力','紧张','悬疑','悲伤','疗愈','浪漫','史诗'],
    '人声': ['纯音乐','女声','男声','童声','合唱','和声','无所谓'],
    '乐器': ['钢琴','吉他','弦乐','合成器','打击','木管','铜管','贝斯','合唱','人声铺底','口风琴'],
    '调式': ['大调','小调','无所谓']
  };
  const GROUPS = ['风格','情绪','人声','乐器','BPM','时长','调式'];

  const BPM_SEG = ["0–60","60–80","80–100","100–120","120–140","140–160","160+"];
  const BPM_MIN = 40, BPM_MAX = 220;
  const segRange = (seg)=>({ "0–60":[0,60],"60–80":[60,80],"80–100":[80,100],"100–120":[100,120],
                              "120–140":[120,140],"140–160":[140,160],"160+":[160,999] }[seg] || [BPM_MIN,BPM_MAX]);

  const DUR_BUCKETS = ["<1 分钟","1–2 分钟","2–3 分钟","3–5 分钟","5 分钟+"];
  const durLabel = (sec)=> sec==null? null : (sec<60? "<1 分钟" : sec<120? "1–2 分钟" : sec<180? "2–3 分钟" : sec<300? "3–5 分钟" : "5 分钟+");

  /* ========== Preferences ========== */
  const QUIET_COUNTS = true;      // 计数默认安静（hover/选中才显）
  const COMPACT = true;           // 紧凑密度
  const SHOW_RECENTS = true;
  const RECENTS_LIMIT = 8;
  const RECENTS_KEY = 'rtf_v3_recents';

  /* ========== Styles (scoped) ========== */
  (function addStyles(){
    if(document.getElementById('rtf-v4-css')) return;
    const css = `
    #pl-chips .rtf { display:flex; flex-direction:column; gap:.7rem; }
#pl-chips .rtf .rt-tab:hover { border-color: rgba(34,211,238,.35); }
    #pl-chips .rtf .rt-selected { display:none; gap:.45rem; align-items:center; flex-wrap:wrap; overflow:auto; }
    #pl-chips .rtf .rt-selected.active { display:flex; }
    #pl-chips .rtf .chip { background:#1b1b21; border:1px solid var(--rt-border, rgba(255,255,255,.14)); color:inherit;
      padding:${COMPACT?'.42rem .7rem':'.55rem .9rem'}; border-radius:999px; font-size:${COMPACT?'.9rem':'.95rem'};
      cursor:pointer; transition: border-color .12s ease, box-shadow .12s ease, background .12s ease; }
    #pl-chips .rtf .chip[aria-pressed="true"] { border-color:#22d3ee; background: rgba(34,211,238,.16); }
    #pl-chips .rtf .chip .count { margin-left:.35em; font-size:.85em; color:rgba(255,255,255,.6); ${QUIET_COUNTS?'opacity:0;':''} transition:opacity .12s ease; }
    #pl-chips .rtf .chip:hover .count, .rtf .chip:focus .count, .rtf .chip[aria-pressed="true"] .count { opacity: .85; }
    #pl-chips .rtf .chip:disabled { opacity:.45; cursor:not-allowed; }
    #pl-chips .rtf .x { margin-left:.35em; opacity:.7; }
    #pl-chips .rtf .rt-tabs { display:flex; gap:.4rem; flex-wrap:wrap; }
    #pl-chips #pl-chips .rtf .rt-tab { background:#141418; border:1px solid var(--rt-border, rgba(255,255,255,.08));
      padding:${COMPACT?'.35rem .6rem':'.4rem .75rem'}; border-radius:10px; cursor:pointer; color:#cfd2d9; }
    #pl-chips .rtf .rt-tab[aria-selected="true"] { color:#fff; border-color:#22d3ee; box-shadow:0 0 0 3px rgba(34,211,238,.16); }
    #pl-chips .rtf .rt-head { display:flex; align-items:center; justify-content:space-between; gap:.6rem; }
    #pl-chips .rtf .muted { color:rgba(255,255,255,.62); }
    #pl-chips .rtf .rt-search { display:none; margin-left:auto; background:#101014; border:1px solid var(--rt-border, rgba(255,255,255,.14));
      color:inherit; padding:.5rem .6rem; border-radius:8px; width:240px; }
    #pl-chips .rtf .rt-sections { display:flex; flex-direction:column; gap:.55rem; }
    #pl-chips .rtf .sec { display:flex; flex-direction:column; gap:.4rem; }
    #pl-chips .rtf .sec-title { color:rgba(255,255,255,.6); font-size:.88rem; font-weight:500; letter-spacing:.02em; }
    #pl-chips .rtf .grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(140px,1fr)); gap:.55rem; }
    #pl-chips .rtf .grid--ellipsis { max-height:120px; overflow:hidden; }
    #pl-chips .rtf .more { align-self:flex-start; background:transparent; border:0; color:rgba(255,255,255,.7); cursor:pointer; padding:.25rem 0; }
    #pl-chips .rtf .rt-actions { display:flex; gap:.5rem; justify-content:flex-end; }
    #pl-chips .rtf .btn { padding:.5rem .85rem; border-radius:10px; border:1px solid var(--rt-border, rgba(255,255,255,.14)); background:#202026; color:inherit; cursor:pointer; }
    #pl-chips .rtf .btn.primary { background:#22d3ee; color:#001015; border-color:transparent; }
    #pl-chips .rtf .rt-quietbar { display:none; padding:.5rem .6rem; border:1px dashed var(--rt-border, rgba(255,255,255,.2)); border-radius:10px; color:rgba(255,255,255,.68); }
    #pl-chips .rtf .rt-quietbar.active { display:block; }
    #pl-chips .rtf .icon-btn { background:#141418; border:1px solid var(--rt-border, rgba(255,255,255,.08)); border-radius:10px; padding:.45rem .6rem; color:#cfd2d9; cursor:pointer; }
    @media (max-width:640px){ .rtf .rt-search{ width:100%; } .rtf .rt-head{ flex-direction:column; align-items:flex-start; } }
    `;
    const s = document.createElement('style'); s.id='rtf-v4-css'; s.textContent = css; document.head.appendChild(s);
  })();

  /* ========== Utilities & Data extraction ========== */
  const $ = (sel, el=document)=> el.querySelector(sel);
  const $$ = (sel, el=document)=> Array.from(el.querySelectorAll(sel));
  const txt = (el)=> (el && (el.textContent||'').trim()) || '';
  const mmss = (s)=>{ const m = (s||'').match(/(\d+):(\d+)/); return m? (+m[1])*60+(+m[2]) : null; };
  const deepCopy = (obj)=> JSON.parse(JSON.stringify(obj));

  function readRows(){
    const tbody = $('tbody', table) || table;
    const rows = $$('tr', tbody).filter(r => !r.classList.contains('placeholder'));
    return rows.map(r=>{
      const tds = $$('td', r);
      const tagsCell = tds[2], bpmCell = tds[3], durCell = tds[4];
      const tagTexts = Array.from(tagsCell ? tagsCell.querySelectorAll('*') : []).map(el=> txt(el)).filter(Boolean);
      const tagsCN = tagTexts.map(t=> EN2CN.get(t) || t);
      const bpm = parseInt(txt(bpmCell), 10);
      const durSec = mmss(txt(durCell));
      return { row:r, tags:tagsCN, bpm: isNaN(bpm)? null:bpm, sec: durSec };
    });
  }

  /* ========== State & history ========== */
  const state = {
    open: '风格',
    selected: { 风格:[], 情绪:[], 人声:null, 乐器:[], BPM:{seg:null,min:null,max:null}, 时长:null, 调式:null },
    qGroup: '',
    history: [] // stack of previous selections
  };
  const pushHistory = ()=>{
    state.history.push(deepCopy(state.selected));
    if(state.history.length>12) state.history.shift();
  };
  const undo = ()=>{
    const prev = state.history.pop();
    if(prev){ state.selected = prev; return true; }
    return false;
  };

  /* ========== Recents ========== */
  const getRecents = ()=>{
    try{ return JSON.parse(localStorage.getItem(RECENTS_KEY)||'{}'); }catch(e){ return {}; }
  };
  const setRecents = (m)=>{
    try{ localStorage.setItem(RECENTS_KEY, JSON.stringify(m)); }catch(e){}
  };
  const addRecent = (g, v)=>{
    if(!SHOW_RECENTS) return;
    const m = getRecents(); m[g] = m[g] || [];
    const list = m[g]; const i = list.indexOf(v);
    if(i>-1) list.splice(i,1);
    list.unshift(v);
    if(list.length>RECENTS_LIMIT) list.length = RECENTS_LIMIT;
    setRecents(m);
  };

  /* ========== Matching & counts ========== */
  const matches = (row)=>{
    const s = state.selected;
    for(const g of ['风格','情绪','乐器']){
      if(s[g].length && !s[g].some(v=> row.tags.includes(v))) return false;
    }
    for(const g of ['人声','时长','调式']){
      if(s[g] && s[g]!=='无所谓'){
        if(g==='时长'){ if(durLabel(row.sec)!==s[g]) return false; }
        else if(!row.tags.includes(s[g])) return false;
      }
    }
    if(s.BPM.seg){
      const [mn,mx] = segRange(s.BPM.seg);
      if(row.bpm==null || row.bpm<mn || row.bpm>mx) return false;
    } else if(s.BPM.min || s.BPM.max){
      const mn = s.BPM.min || BPM_MIN, mx = s.BPM.max || BPM_MAX;
      if(row.bpm==null || row.bpm<mn || row.bpm>mx) return false;
    }
    return true;
  };

  function computeCounts(group, items){
    const prev = deepCopy(state.selected);
    if(group==='BPM'){ prev.BPM = {seg:null,min:null,max:null}; }
    else if(['人声','时长','调式'].includes(group)){ prev[group] = null; }
    else { prev[group] = []; }

    const base = items.filter(r=>{ const save=state.selected; state.selected=prev; const ok=matches(r); state.selected=save; return ok; });

    const counts = {};
    if(group==='BPM'){
      BPM_SEG.forEach(seg=> counts[seg]=0);
      base.forEach(r=>{ if(r.bpm==null) return; for(const seg of BPM_SEG){ const [mn,mx] = segRange(seg); if(r.bpm>=mn && r.bpm<=mx){ counts[seg]++; break; } } });
      return counts;
    }
    if(group==='时长'){
      DUR_BUCKETS.forEach(b=> counts[b]=0);
      base.forEach(r=>{ const b = durLabel(r.sec); if(b) counts[b]++; });
      return counts;
    }
    const values = group==='调式'? TAGS['调式'] : group==='人声'? TAGS['人声'] : group==='乐器'? TAGS['乐器'] : group==='情绪'? TAGS['情绪'] : TAGS['风格'];
    values.forEach(v=> counts[v]=0);
    base.forEach(r=>{ values.forEach(v=>{ if(r.tags.includes(v)) counts[v]++; }); });
    return counts;
  }

  /* ========== Rendering helpers ========== */
  function chip(label, checked, count, disabled){
    const b = document.createElement('button'); b.type='button'; b.className='chip';
    b.setAttribute('aria-pressed', checked?'true':'false');
    b.textContent = label;
    if(count>0){ const c = document.createElement('span'); c.className='count'; c.textContent=count; b.appendChild(c); }
    if(disabled){ b.disabled = true; b.title = '当前组合下暂无结果'; }
    return b;
  }

  function renderSelected(){
    const sel = document.getElementById('rt-selected');
    sel.innerHTML = '';
    const chips = [];
    ['风格','情绪','乐器'].forEach(g=> state.selected[g].forEach(v=> chips.push({g,label:v})));
    ['人声','时长','调式'].forEach(g=>{ const v=state.selected[g]; if(v && v!=='无所谓') chips.push({g,label:v}); });
    if(state.selected.BPM.seg) chips.push({g:'BPM',label:'BPM '+state.selected.BPM.seg});
    else if(state.selected.BPM.min||state.selected.BPM.max){
      const mn = state.selected.BPM.min||BPM_MIN, mx = state.selected.BPM.max||BPM_MAX; chips.push({g:'BPM',label:`BPM ${mn}-${mx}`});
    }
    chips.forEach(ch=>{
      const c = document.createElement('button'); c.className='chip'; c.textContent = ch.label;
      const x = document.createElement('span'); x.className='x'; x.textContent='×'; c.appendChild(x);
      c.onclick = ()=>{ pushHistory(); if(ch.g==='BPM'){ state.selected.BPM = {seg:null,min:null,max:null}; }
        else if(['风格','情绪','乐器'].includes(ch.g)){ state.selected[ch.g] = state.selected[ch.g].filter(v=> v!==ch.label); }
        else { state.selected[ch.g] = null; } applyFilter(); };
      sel.appendChild(c);
    });
    if(chips.length){
      const clear = document.createElement('button'); clear.className='btn'; clear.textContent='清空全部';
      clear.onclick = ()=>{ pushHistory(); state.selected = { 风格:[], 情绪:[], 人声:null, 乐器:[], BPM:{seg:null,min:null,max:null}, 时长:null, 调式:null }; applyFilter(); };
      sel.appendChild(clear);
      sel.classList.add('active');
    }else sel.classList.remove('active');
  }

  function orderValues(group, values, counts){
    // 选中优先 -> 最近使用 -> (无搜索且为全部段) 按计数降序 -> 其余按原顺序
    const selectedSet = new Set(state.selected[group] || []);
    const recents = (getRecents()[group] || []).filter(v=> values.includes(v));
    const recentSet = new Set(recents);
    const withMeta = values.map(v=> ({v, selected: selectedSet.has(v), recent: recentSet.has(v), cnt: (counts[v]||0)}));
    withMeta.sort((a,b)=>{
      if(a.selected!==b.selected) return a.selected? -1:1;
      if(a.recent!==b.recent) return a.recent? -1:1;
      if(state.qGroup===''){ // only when未搜索
        if(b.cnt!==a.cnt) return b.cnt - a.cnt;
      }
      return 0;
    });
    return withMeta.map(x=> x.v);
  }

  function renderPanel(items){
    document.querySelectorAll('.rt-tab').forEach(t=> t.setAttribute('aria-selected', t.dataset.k===state.open?'true':'false'));
    renderSelected();

    const panel = document.getElementById('rt-panel');
    panel.innerHTML = '';

    // header
    const head = document.createElement('div'); head.className='rt-head';
    const title = document.createElement('div'); title.innerHTML = `<span class="muted">当前：${state.open}</span>`;
    const searchBtn = document.createElement('button'); searchBtn.className='icon-btn'; searchBtn.textContent='搜索';
    const search = document.createElement('input'); search.className='rt-search'; search.placeholder='在当前分组内搜索…';
    head.append(title, searchBtn, search); panel.appendChild(head);

    const enableSearch = ['风格','情绪','乐器'].includes(state.open);
    let searchOn = !!state.qGroup;
    const setSearchVisible = (v)=>{ search.style.display = v && enableSearch ? '' : 'none'; searchBtn.setAttribute('aria-pressed', v?'true':'false'); };
    setSearchVisible(searchOn);
    searchBtn.onclick = ()=>{ searchOn=!searchOn; setSearchVisible(searchOn); if(searchOn) search.focus(); else { state.qGroup=''; renderPanel(items); } };
    search.value = state.qGroup || '';
    search.oninput = ()=>{ state.qGroup = search.value.trim(); renderPanel(items); };

    const counts = computeCounts(state.open, items);
    const sections = document.createElement('div'); sections.className='rt-sections';

    // 空结果提示（更安静，仅在结果为 0 时显示）
    const visibleRows = items.filter(matches).length;
    const quiet = document.createElement('div'); quiet.className='rt-quietbar';
    if(visibleRows===0){
      quiet.classList.add('active');
      const undoBtn = document.createElement('button'); undoBtn.className='btn'; undoBtn.textContent='撤销上一步';
      undoBtn.onclick = ()=>{ if(undo()) applyFilter(); };
      const clearBtn = document.createElement('button'); clearBtn.className='btn'; clearBtn.textContent='清空全部';
      clearBtn.onclick = ()=>{ pushHistory(); state.selected = { 风格:[], 情绪:[], 人声:null, 乐器:[], BPM:{seg:null,min:null,max:null}, 时长:null, 调式:null }; applyFilter(); };
      quiet.innerHTML = '没有符合条件的结果。';
      quiet.append(' ', undoBtn, ' ', clearBtn);
    }
    panel.appendChild(quiet);

    if(state.open==='BPM'){
      const sec1 = document.createElement('div'); sec1.className='sec'; sec1.innerHTML = `<div class="sec-title">快速选择</div>`;
      const grid1 = document.createElement('div'); grid1.className='grid';
      BPM_SEG.forEach(seg=>{
        const cnt = counts[seg]||0; const checked = state.selected.BPM.seg===seg;
        const btn = chip(seg, checked, cnt, !cnt && !checked);
        btn.onclick = ()=>{ pushHistory(); state.selected.BPM = {seg, min:null, max:null}; addRecent('BPM', seg); applyFilter(); };
        grid1.appendChild(btn);
      });
      sec1.appendChild(grid1); sections.appendChild(sec1);

      const sec2 = document.createElement('div'); sec2.className='sec'; sec2.innerHTML = `<div class="sec-title">自定义区间</div>`;
      const area = document.createElement('div'); area.className='grid';
      const iMin = document.createElement('input'); iMin.type='number'; iMin.placeholder=BPM_MIN; iMin.min=BPM_MIN; iMin.max=BPM_MAX; iMin.className='rt-search';
      const iMax = document.createElement('input'); iMax.type='number'; iMax.placeholder=BPM_MAX; iMax.min=BPM_MIN; iMax.max=BPM_MAX; iMax.className='rt-search';
      iMin.value = state.selected.BPM.min ?? ''; iMax.value = state.selected.BPM.max ?? '';
      const applyRange = ()=>{
        pushHistory();
        let mn = parseInt(iMin.value||BPM_MIN,10), mx = parseInt(iMax.value||BPM_MAX,10);
        if(isNaN(mn)) mn=BPM_MIN; if(isNaN(mx)) mx=BPM_MAX; if(mn>mx) [mn,mx] = [mx,mn];
        mn = Math.max(BPM_MIN, Math.min(mn, BPM_MAX)); mx = Math.max(BPM_MIN, Math.min(mx, BPM_MAX));
        state.selected.BPM = {seg:null, min:mn, max:mx}; applyFilter();
      };
      iMin.onchange = applyRange; iMax.onchange = applyRange;
      area.append('从 ', iMin, ' 到 ', iMax); sec2.appendChild(area); sections.appendChild(sec2);
    }
    else if(state.open==='时长' || state.open==='人声' || state.open==='调式'){
      const valuesBase = state.open==='时长' ? DUR_BUCKETS : TAGS[state.open];
      // 排序：选中优先 → 最近使用 → 计数（无搜索时）
      const values = orderValues(state.open, valuesBase, counts);
      const sec = document.createElement('div'); sec.className='sec';
      const grid = document.createElement('div'); grid.className='grid';
      const selected = state.selected[state.open];
      values.forEach(v=>{
        if(state.qGroup && !v.includes(state.qGroup)) return;
        const cnt = counts[v]||0; const checked = selected===v;
        const btn = chip(v, checked, cnt, !cnt && !checked);
        btn.onclick = ()=>{ pushHistory(); state.selected[state.open] = (state.selected[state.open]===v ? null : v); addRecent(state.open, v); applyFilter(); };
        grid.appendChild(btn);
      });
      sec.appendChild(grid); sections.appendChild(sec);
    }
    else {
      const values = TAGS[state.open];
      const rec = (getRecents()[state.open] || []).filter(v=> values.includes(v));
      const common = values.slice(0,6);
      const rest = values.filter(v=> !common.includes(v) && !rec.includes(v));
      const orderedRest = orderValues(state.open, rest, counts);

      const addSection = (titleStr, arr, collapsible=false)=>{
        if(!arr || !arr.length) return;
        const sec = document.createElement('div'); sec.className='sec';
        if(titleStr) sec.innerHTML = `<div class="sec-title">${titleStr}</div>`;
        const grid = document.createElement('div'); grid.className='grid';
        if(collapsible) grid.classList.add('grid--ellipsis');
        arr.forEach(v=>{
          if(state.qGroup && !v.includes(state.qGroup)) return;
          const cnt = counts[v]||0; const checked = state.selected[state.open].includes(v);
          const btn = chip(v, checked, cnt, !cnt && !checked);
          btn.onclick = ()=>{
            pushHistory();
            const list = state.selected[state.open];
            const i = list.indexOf(v); (i>-1)? list.splice(i,1): list.push(v);
            addRecent(state.open, v);
            applyFilter();
          };
          grid.appendChild(btn);
        });
        sec.appendChild(grid);
        if(collapsible){
          const more = document.createElement('button'); more.className='more'; more.textContent='展开更多';
          let expanded=false; more.onclick = ()=>{ expanded=!expanded; grid.classList.toggle('grid--ellipsis', !expanded); more.textContent = expanded?'收起':'展开更多'; };
          sec.appendChild(more);
        }
        sections.appendChild(sec);
      };

      if(!state.qGroup){
        addSection('最近使用', rec, false);
        addSection('常用', common, false);
        addSection('全部', orderedRest, true);
      } else {
        const orderedAll = orderValues(state.open, values, counts);
        addSection('', orderedAll, false);
      }
    }

    panel.appendChild(sections);

    // actions
    const actions = document.createElement('div'); actions.className='rt-actions';
    const undoBtn = document.createElement('button'); undoBtn.className='btn'; undoBtn.textContent='撤销上一步';
    undoBtn.onclick = ()=>{ if(undo()) applyFilter(); };
    const resetBtn = document.createElement('button'); resetBtn.className='btn'; resetBtn.textContent='重置当前分组';
    resetBtn.onclick = ()=>{
      pushHistory();
      const k = state.open;
      if(k==='BPM') state.selected.BPM={seg:null,min:null,max:null};
      else if(['人声','时长','调式'].includes(k)) state.selected[k]=null;
      else state.selected[k]=[];
      applyFilter();
    };
    const okBtn = document.createElement('button'); okBtn.className='btn primary'; okBtn.textContent='完成';
    okBtn.onclick = ()=>{/* 即时生效，此按钮为弱确认，可留空或收起抽屉型面板 */};
    actions.append(undoBtn, resetBtn, okBtn);
    panel.appendChild(actions);
  }

  /* ========== Root build & init ========== */
  function buildRoot(){
    mount.innerHTML = '';
    const root = document.createElement('div'); root.className='rtf';

    const sel = document.createElement('div'); sel.className='rt-selected'; sel.id='rt-selected'; root.appendChild(sel);

    const tabs = document.createElement('div'); tabs.className='rt-tabs';
    GROUPS.forEach(k=>{
      const b = document.createElement('button'); b.className='rt-tab'; b.type='button'; b.dataset.k=k; b.textContent=k;
      b.setAttribute('aria-selected', k===state.open? 'true':'false');
      b.onclick = ()=>{ state.open=k; renderPanel(readRows()); };
      tabs.appendChild(b);
    });
    root.appendChild(tabs);

    const panel = document.createElement('div'); panel.id='rt-panel'; root.appendChild(panel);
    mount.appendChild(root);
  }

  function applyFilter(){
    const items = readRows();
    let shown = 0;
    items.forEach(it=>{
      if(matches(it)){ it.row.style.display=''; shown++; } else it.row.style.display='none';
    });
    renderPanel(items);
    const hc = document.getElementById('hero-count'); if(hc){ hc.textContent = hc.textContent.replace(/\d+\s*首/,(shown+' 首')); }
  }

  // Wait rows ready
  const mo = new MutationObserver(()=>{
    const ready = document.querySelectorAll('.table tbody tr').length || document.querySelectorAll('.table tr').length;
    if(ready){ mo.disconnect(); buildRoot(); applyFilter(); }
  });
  mo.observe(table, {childList:true, subtree:true});
  if(document.querySelectorAll('.table tbody tr').length || document.querySelectorAll('.table tr').length){ buildRoot(); applyFilter(); }
})();

#pl-chips .rtf .grid .chip{ width:100%; text-align:center; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }

/* tidy grid chip paddings */
#pl-chips .rtf .grid .chip{ padding:.5rem .75rem; border-radius:999px; }
